有一种错叫持有锁
有一种错叫
持有锁
记得有一次在M国出差时,偶遇N君,周末一起到O州之国家公园爬山。山中漫步时,大家天南海北地闲聊,聊的内容大多都不记得了。今天任然记得的是,N君偶发感慨:“M国是很讲规则的地方,在这里没有钱不会被人瞧不起,但是不懂规则就会被人瞧不起……”其实,不仅M国如此,软件世界也是很讲规则的地方。
“
oops和panic
”
以Linux为例,当内核空间的代码违反规则时,轻则有oops警告,如果严重了,则有系统Panic。而一旦进入Panic流程,则只能玉石俱焚,系统重启。
内核有一个名为panic_on_oops的变量,当这个变量为1时,所有oops都会升级为Panic。对于可靠性要求高的系统,这个变量一般是设为1的。
panic_on_oops
=============
Controls the kernel's behaviour when an oops or BUG is encountered.
= ===================================================================
0 Try to continue operation.
1 Panic immediately. If the `panic` sysctl is also non-zero then the
machine will be rebooted.
= ===================================================================
名字与panic_on_oops类似的内核变量还有很多,比如:
panic_on_stackoverflow
panic_on_unrecovered_nmi
panic_on_warn
panic_on_rcu_stall
panic_on_io_nmi
panic_on_taint
这么多panic_on变量也从侧面说明了内核的规则很多。在内核代码里搜索oops,则可以搜到更多的内核规则。
“
oops连续剧
”
因为代码里有这么多的oops逻辑埋伏着,所以在实践中,遇到oops也是常有的事。对于一台Linux机器,我很喜欢浏览它的内核消息,在观察内核消息时,经常可以看到各种oops。最近在开发幽兰的新镜像时,也遇到一串oops。说一串oops,是因为这个oops是像连续剧一样,有很多“集”。每一集主题相同,但是“剧情”有所不同。
比如,下面是第一集:
[ 8.089900] 1 lock held by kworker/4:1/54:
[ 8.089902] #0: ffffff8105c121a0 (&rport->mutex){....}-{3:3}, at: rockchip_chg_detect_work+0x2c4/0x540
[ 8.089920] CPU: 4 PID: 54 Comm: kworker/4:1 Not tainted 5.10.110 #6
[ 8.089922] Hardware name: Rockchip RK3588 code book YourLand (DT)
[ 8.089926] Workqueue: events rockchip_chg_detect_work
[ 8.089930] Call trace:
[ 8.089933] dump_backtrace+0x0/0x210
[ 8.089936] show_stack+0x2c/0x38
[ 8.089940] dump_stack_lvl+0xd4/0xf8
[ 8.089942] dump_stack+0x14/0x30
[ 8.089945] process_one_work+0x404/0x5a0
[ 8.089947] worker_thread+0x48/0x460
[ 8.089950] kthread+0x128/0x130
[ 8.089953] ret_from_fork+0x10/0x1c
就像写文章有套路一样,Oops信息也有相对固定的格式,一般包含如下几个部分:
所犯错误,或者说“罪名”
发生地,包括CPU,当前进程,系统信息等
调用栈
寄存器信息
其它现场信息
对于本例,内核给出的罪名如下:
1 lock held by kworker/4:1/54
直接翻译便是“1个锁被kworker/4:1/54所持有”。
“
罪出何名
”
在一些地方,持枪是犯法的,但是这里说的是持有锁,持有锁也犯法么?
根据上面的信息搜索内核代码,可以找到打印这个信息的地方,即:
printk("%d lock%s held by %s/%d:\n", depth,
depth > 1 ? "s" : "", p->comm, task_pid_nr(p));
根据这个printk的写法,可以知道罪名信息中的54是线程ID,kworker/4:1是系统线程的线程名。
根据调用栈的process_one_work可以找到发起这次“兴师问罪”行动的地方:
if (unlikely(in_atomic() || lockdep_depth(current) > 0)) {
debug_show_held_locks(current);
dump_stack();
}
其中的debug_show_held_locks函数实现在kernel\locking\lockdep.c中,这个.c有6000多行C代码,里面有很多函数都是审计纠错的。文件开头的描述也言简意赅地表达了这个目的:
Runtime locking correctness validator
代码的作者是Linux内核圈里的名人:因戈·莫而纳(Ingo Molnar)。
2011年内核峰会上的因戈·莫而纳
(照片来自LWN)
/*
* Careful: only use this function if you are sure that
* the task cannot run in parallel!
*/
void debug_show_held_locks(struct task_struct *task)
{
if (unlikely(!debug_locks)) {
printk("INFO: lockdep is turned off.\n");
return;
}
lockdep_print_held_locks(task);
}
EXPORT_SYMBOL_GPL(debug_show_held_locks);
值得说明的是:内核的这个“锁监督机制”被视为一种高端服务,一旦内核有污点,那么这个服务可能被取消。比如下面这几句来自其它系统的内核消息表示,因为加载了nvidia协议的驱动,污染了内核,因为此锁调试服务被禁止了。
[ 0.923328] nvidia: loading out-of-tree module taints kernel.
[ 0.923330] nvidia: module license 'NVIDIA' taints kernel.
[ 0.923331] Disabling lock debugging due to kernel taint
“
代码源头
”
再回到我们的问题,看来是触发了内核锁监督机制。在罪名信息的下面一行,打印了这起事故涉及的锁:
#0: ffffff8105c121a0 (&rport->mutex){....}-{3:3}, at: rockchip_chg_detect_work+0x2c4/0x540
上面信息分为如下几个部分:
锁的序号,当涉及多个锁时,依次排列
锁对象的地址
锁的名字
执行加锁动作的代码地址
后三部分信息来自如下代码:
printk(KERN_CONT "%px", hlock->instance);
print_lock_name(lock);
printk(KERN_CONT ", at: %pS\n", (void *)hlock->acquire_ip);
其中的print_lock_name函数也是出自因戈之手,摘录如下:
static void print_lock_name(struct lock_class *class)
{
char usage[LOCK_USAGE_CHARS];
get_usage_chars(class, usage);
printk(KERN_CONT " (");
__print_lock_name(class);
printk(KERN_CONT "){%s}-{%d:%d}", usage,
class->wait_type_outer ?: class->wait_type_inner,
class->wait_type_inner);
}
根据加锁函数的信息,可以找到持锁的代码,来自瑞芯微。
在1392行,果然有加锁动作。
虽然这个函数中有解锁调用,但是解锁动作是在case语句里的。也就是说可能只加锁,不解锁。在函数末尾的注释明确描述了这个特征:
/*
* Hold the mutex lock during the whole charger
* detection stage, and release it after detect
* the charger type.
*/
schedule_delayed_work(&rport->chg_work, delay);
}
意思是:在整个充电器检测阶段都持有锁,直到检测到充电器类型才释放。
糊涂啊,这显然是和内核的锁政策对抗啊。上面的函数是以“作业”的形式提交给内核的,由内核的工作线程来执行。工作线程在调用作业函数后,例行检查是否有锁违规,结果被查到了。
搜索内核消息,可以看到被查到很多次。
阅读持锁代码所在源文件的其它代码,可以看到它管理的是幽兰的USB PHY设备,与充电逻辑有关。源文件名中的inno应该是PHY芯片的厂商名。
芯动科技有限公司(Innosilicon)是世界先进、国内领军的高端混合电路芯片设计公司,中国高速芯片技术市场领导企业,在全球范围内拥有核心竞争优势。
“
回避解法
”
对于这个问题,硬件伙伴认为是格蠹新增的内核选项导致的,因为他们那里看不到这些oops。顺着这个线索追查,的确是与内核的编译选项有关。格蠹的内核编译选项新增了如下两个:
CONFIG_LOCKDEP=y
CONFIG_LOCK_STAT=y
其中的CONFIG_LOCKDEP就是用来开启上面说的锁调试功能的。如果把这个选项设置为n,那么内核消息中的oops就没有了。
但这样做其实是禁止了锁监督机制,而幽兰的内核,是不想关闭这个选项的,因为关闭这个选项意味着放弃了内核的一项高端服务。而这个服务对于发现内核代码的设计不足是有益的。
从表面看,这个服务只是打印一些警告信息,但从深层说,它代表着对规则的重视和守护。而幽兰的内核是看重规则的。如本文开头所言,对规则的重视程度代表了一个系统的文明程度和价值取向。当一个行为与规则矛盾时,应该纠正行为,而不是要禁止规则。
“
天下无锁
”
搜索包含问题代码的源文件名,发现就在前几天,有个内核补丁刚好是关于这个文件的。
顺着这个补丁看内核代码树的代码,与本地代码比较,很容易发现,在新的代码中,加锁的那些行已经不见了。如此看来,已经有人发现了这个持锁问题,并且做了修正。根据新代码调整本地代码,编译更新后,oops不见了。
读到这里,有格友可能会心生疑惑,难道内核代码不可以持有锁吗?当然不是。问题的关键是持有锁的时间长短。总的来说,锁是用来保护共享资源的,对这些资源的占用代表着对公共资源的占用。对于这种占有,时间要尽可能短,不应该拿到了就不释放,长时间占着。好比是办公大楼里的卫生间,大楼里的每个人都有使用卫生间的权利,但是把卫生间当作个人休息室,进去了在里面刷屏看剧就不对了。为此,Linux内核设计了监督机制,在某段代码即将“赋闲”时,对其做检查,如果发现它手里还持有锁,则给予警告:
“你都要失去执行权了,为什么还持锁不放?”
“我等会儿还用……”
“等会儿还用,就该等会儿再排队获取……”
从珍惜公共资源的角度看,内核的这个检查是健康且必要的。
-END-
购买幽兰代码本即可成为兰舍会员,与众多技术高手一起成长。
购买可前往淘宝格友小店:
https://m.tb.cn/h.Uuv7fit?tk=N1iIdn8t4CI
盛格塾是格蠹科技旗下的知识分享平台,是以“格物致知”为教育理念的现代私塾。
本着为先圣继绝学的思想,盛格塾努力将传统文化中的精华与现代科技密切结合,以传统文化和人文情怀阐释现代科技,用现代科技传播传统文化。
访问方式
手机端:微信小程序搜索“盛格塾”
电脑端:下载Nano Code社区版客户端
https://nanocode.cn/#/download
格友公众号
盛格塾小程序
往期 · 精彩推荐